(function(window){

  /**
   * @description 首先是一个字符串的扫描方法
   * @param {String} str 
   */
  function Scanner(str) {
    this.templatestr = str;
    this.pos = 0;
    this.tail = str;
  }

  Scanner.prototype.eos = function() {
    return this.pos >= this.templatestr.length;
  }
  Scanner.prototype.scan = function (tag) {
    if (this.tail.indexOf(tag) == 0) {
      this.pos += tag.length;
      this.tail = this.templatestr.substring(this.pos);
    }
  }
  Scanner.prototype.scanUntil = function (stopTag) {
    const startPos = this.pos;
    while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
      this.pos++;
      this.tail = this.templatestr.substring(this.pos);
    }
    return this.templatestr.substring(startPos, this.pos);
  }


  /**
   * @description 在对象中查找 keyname 表达式作为属性的属性值
   * @param {Object} dataobj 
   * @param {String} keyname 
   */
  function lookup (dataobj, keyname) {
    //console.log(dataobj, keyname);
    if (keyname != '.' && keyname.includes('.')) {
      let midtemp = dataobj;
      let arr = keyname.split('.');

      for (let i = 0, len = arr.length; i < len; i++) {
        let key = arr[i];
        if (midtemp[key]) {
          midtemp = midtemp[key];
        } else {
          return undefined;
        }
      }
      return midtemp;
    }
    return dataobj[keyname];
  }

  /**
   * @description 整理一元的 tokens
   * @param {Array} tokens 
   */
  function nestTokens (tokens) {
    let nestedToken = [];
    let collector = nestedToken;
    let sections = []; // 栈

    let token, section;
    for (let i = 0, len = tokens.length; i < len; ++i) {
      // 遍历每一个token
      token = tokens[i];

      switch (token[0]) {
        case '#':
          collector.push(token);
          // 一个显然的过程，每碰到一次'#'就要把当前的token入栈
          sections.push(token);
          // 接下来的神来之笔，将collector指向这个 token 的第三位，开始收集子 tokens
          collector = token[2] = [];
          break;
        case '/':
          // 出栈，返回上一层
          // 如果栈空，意味着到目前为止#和/成对出现且全部抵消了
          section = sections.pop();
          collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedToken;
          break;
        default:
          collector.push(token);
      }
    }
    return nestedToken;
  }

  /**
   * @description 将字符串转化为原始的tokens
   * @param {String} str 
   */
  function parseTemplate (str) {
    let tokens = [];

    // 开始扫描
    const scanner = new Scanner(str);

    while (!scanner.eos()) {
      tokens.push(['text', scanner.scanUntil('{{').trim()]);
      scanner.scan('{{');
      // 注意，如果没有{{，此时已经走到头，再执行会返回空字符串，所以加个判断
      if (!scanner.eos()) {
        let cur = scanner.scanUntil('}}').trim(); // 去空格，有人为了美观会在大胡子里用空格
        if (cur[0] == '#') {
          tokens.push(['#', cur.substring(1)]);
        } else if (cur[0] == '/') {
          tokens.push(['/', cur.substring(1)]);
        } else {
          tokens.push(['name', cur]);
        }
        scanner.scan('}}');
      }
    }

    return nestTokens(tokens);
  }

  /**
   * @description ['#', 'arr', Array(m)]
   * @param {array} token 注意不是tokens
   * @param {object} data 数据
   */
  function parseArray (token, data) {
    // 此时v是数组，或者布尔值
    let v = lookup(data, token[1]);

    let ret = '';

    for (let i = 0, len = v.length; i < len; i++) {
      // 因为 renderTemplate 只能接受对象，所以自然的
      // 子数组无法处理，'.'通配符也无法识别
      // 所以给 v[i] 对象增加一个'.'属性……
      ret += renderTemplate(token[2], {
        ...v[i],
        '.': v[i]
      });
    }

    return ret;
  }

  /**
   * @description 渲染字符串
   * @param {array} tokens 数组
   * @param {object} data 对象，不是数组
   */
  function renderTemplate (tokens, data) {
    let ret = '';

    for (let i = 0, len = tokens.length; i < len; ++i) {
      let token = tokens[i];
      if (token[0] == 'text') {
        ret += token[1];
      } else if (token[0] == 'name') {
        ret += lookup(data, token[1]);
      } else if (token[0] == '#') {
        // 开始递归处理有标记符的 token
        ret += parseArray(token, data);
      }
    }

    return ret;
  }

  
  window.$render = function(str, data) {
    return renderTemplate(parseTemplate(str), data);
  };  
})(window);
